package com.gt.quality.system.login;

import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.ShearCaptcha;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.gt.quality.base.BusinessSelfException;
import com.gt.quality.config.SaTokenModelConfig;
import com.gt.quality.config.utils.LoginHelper;
import com.gt.quality.config.utils.LoginUser;
import com.gt.quality.config.utils.RoleDTO;
import com.gt.quality.config.utils.TenantConstants;
import com.gt.quality.config.utils.UserConstants;
import com.gt.quality.system.entity.SysDept;
import com.gt.quality.system.entity.SysLogininfor;
import com.gt.quality.system.entity.SysMenu;
import com.gt.quality.system.entity.SysRole;
import com.gt.quality.system.entity.SysUser;
import com.gt.quality.system.entity.vo.LoginViewBody;
import com.gt.quality.system.entity.vo.LoginVo;
import com.gt.quality.base.pojo.Resp;
import com.gt.quality.system.service.SysDeptService;
import com.gt.quality.system.service.SysLogininforService;
import com.gt.quality.system.service.SysMenuService;
import com.gt.quality.system.service.SysRoleService;
import com.gt.quality.system.service.SysUserService;
import com.gt.quality.utils.AddressUtils;
import com.gt.quality.utils.RSAUtil;
import com.gt.quality.utils.ServletUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.Normalizer;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author Administrator
 */
@RestController
@RequestMapping("/saToken")
@Slf4j
@Tag(name = "security-login")
public class LoginController {

    private SysRoleService sysRoleService;

    @Resource
    @Qualifier("threadPoolTaskExecutor")
    private TaskExecutor threadPoolTaskExecutor;

    @Autowired
    public void setSysRoleService(SysRoleService sysRoleService) {
        this.sysRoleService = sysRoleService;
    }

    private SysMenuService sysMenuService;

    @Autowired
    public void setSysMenuService(SysMenuService sysMenuService) {
        this.sysMenuService = sysMenuService;
    }

    private SysDeptService sysDeptService;

    @Autowired
    public void setSysDeptService(SysDeptService sysDeptService) {
        this.sysDeptService = sysDeptService;
    }

    private SysUserService sysUserService;

    @Autowired
    public void setSysUserService(SysUserService sysUserService) {
        this.sysUserService = sysUserService;
    }

    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Value("${sa-token.open-verify-code:false}")
    private boolean enableVerifyCode;

    private SaTokenModelConfig saTokenModelConfig;

    @Autowired
    public void setSaTokenModelConfig(SaTokenModelConfig saTokenModelConfig) {
        this.saTokenModelConfig = saTokenModelConfig;
    }

    private SysLogininforService sysLogininforService;

    @Autowired
    public void setSysLogininforService(SysLogininforService sysLogininforService) {
        this.sysLogininforService = sysLogininforService;
    }

    @PostMapping(value = "/user/login", consumes = "application/json")
    @Operation(summary = "JSON登录接口")
    public Resp<LoginViewBody> loginJsonUp(@RequestBody LoginVo user, HttpServletRequest request) {
        return loginUp(user, request);
    }

    @GetMapping(value = "/user/createKey")
    @Operation(summary = "秘钥加密")
    @SaIgnore
    public Resp<String> createKey(@RequestParam("key") String key) throws Exception {
        // RSA 私钥解密
        String password = RSAUtil.encryptByPublicKey(RSAUtil.pubKey, key);
        return Resp.ok(password);
    }

    // 登录接口
    @PostMapping(value = "/user/loginByForm", consumes = "application/x-www-form-urlencoded")
    @Operation(summary = "FROM登录接口")
    public Resp<LoginViewBody> loginUp(LoginVo user, HttpServletRequest request) {
        String remoteAddr = getIpAddress(request);
        try {
            if (enableVerifyCode) {
                // 验证码校验
                String verify = user.getVerify();
                if (StringUtils.isEmpty(verify)) {
                    String error = "验证码不能为空";
                    handlerFail(request, user, error);
                    return Resp.error(error);
                }
                Object verifyCode = redisTemplate.opsForValue().get(remoteAddr + "_verifyCode");
                if (verifyCode == null) {
                    String error = "验证码已经过期";
                    handlerFail(request, user, error);
                    return Resp.error(error);
                }
                if (!verifyCode.toString().equalsIgnoreCase(verify)) {
                    String error = "验证码不正确";
                    handlerFail(request, user, error);
                    return Resp.error(error);
                }
            }
            // RSA 私钥解密
            String aesBackPwd = RSAUtil.decryptByPrivateKey(RSAUtil.priKey, user.getPassword());
            String password = SecureUtil.md5(aesBackPwd);
            user.setPassword(password);
        } catch (Exception e) {
            String error = "密码解密失败错误，验证签名加密是否合法";
            handlerFail(request, user, error);
            log.error("{}---->{}", error, e.getMessage());
            return Resp.error(error);
        }
        // 校验Xss参数
        checkXssParams(user);
        SysUser loginUser = sysUserService.findUserByNameAndPassword(user.getLoginAccount(), user.getPassword());
        if (loginUser != null) {
            LoginViewBody loginView = getLoginViewBody(request, loginUser, remoteAddr);
            // 第3步，返回给前端
            sysLogininforService.save(getLoginInfor(request, user.getLoginAccount(), "0", "登录成功"));
            return Resp.ok(loginView);
        }
        String error = "用户名或密码错误";
        handlerFail(request, user, error);
        return Resp.error(error);
    }

    public void handlerFail(HttpServletRequest request, LoginVo user, String msg) {
        sysLogininforService.save(getLoginInfor(request, user.getLoginAccount(), "1", msg));
    }

    public SysLogininfor getLoginInfor(HttpServletRequest request, String userName, String status, String msg) {
        SysLogininfor logininfor = new SysLogininfor();
        logininfor.setUserName(userName);
        logininfor.setClientKey("e5cd7e4891bf95d1d19206ce24a7b32e");
        logininfor.setDeviceType("");
        String ip = ServletUtils.getClientIP();
        String address = AddressUtils.getRealAddressByIP(ip);
        logininfor.setIpaddr(ip);
        logininfor.setLoginLocation(address);
        logininfor.setBrowser(getBrowserType(request));
        logininfor.setOs(getSimpleOs(request));
        // 登录状态（0成功 1失败）
        logininfor.setStatus(status);
        logininfor.setMsg(msg);
        logininfor.setLoginTime(LocalDateTime.now());
        return logininfor;
    }

    public String getSimpleOs(HttpServletRequest request) {
        String ua = request.getHeader("User-Agent");
        if (ua == null) {
            return "Unknown";
        }
        ua = ua.toLowerCase();

        if (ua.contains("windows")) {
            return "Windows";
        } else if (ua.contains("mac")) {
            return "Mac OS";
        } else if (ua.contains("linux")) {
            return "Linux";
        } else if (ua.contains("android")) {
            return "Android";
        } else if (ua.contains("iphone") || ua.contains("ipad")) {
            return "iOS";
        } else if (ua.contains("unix")) {
            return "Unix";
        }
        return "Unknown";
    }

    public String getBrowserType(HttpServletRequest request) {
        String userAgent = request.getHeader("User-Agent");
        if (userAgent == null) {
            return "Unknown";
        }
        if (userAgent.contains("Chrome") && !userAgent.contains("Edg")) {
            return "Chrome";
        } else if (userAgent.contains("Firefox")) {
            return "Firefox";
        } else if (userAgent.contains("Safari") && !userAgent.contains("Chrome")) {
            return "Safari";
        } else if (userAgent.contains("Edg")) {
            return "Edge";
        } else if (userAgent.contains("Trident") || userAgent.contains("MSIE")) {
            return "Internet Explorer";
        }
        return "Unknown";
    }

    /**
     * 构建登录实体
     *
     * @param request    请求
     * @param loginUser  登录用户
     * @param remoteAddr 远程地址
     * @return 登录实体
     */
    public LoginViewBody getLoginViewBody(HttpServletRequest request, SysUser loginUser, String remoteAddr) {
        SysDept dept = sysDeptService.getById(loginUser.getDeptId());
        LoginUser sessionUser = new LoginUser();
        sessionUser.setTenantId(loginUser.getTenantId());
        sessionUser.setUserId(loginUser.getUserId());
        sessionUser.setDeptId(loginUser.getDeptId());
        sessionUser.setDeptCategory(dept.getDeptCategory());
        sessionUser.setDeptName(dept.getDeptName());
        sessionUser.setUserType(loginUser.getUserType());
        sessionUser.setLoginTime(System.currentTimeMillis());
        sessionUser.setExpireTime(System.currentTimeMillis() + (30 * 60 * 1000));
        sessionUser.setIpaddr(remoteAddr);
        sessionUser.setLoginLocation("中国");
        sessionUser.setBrowser(getBrowserType(request));
        sessionUser.setOs(getOs(request));
        sessionUser.setUsername(loginUser.getUserName());
        sessionUser.setNickname(loginUser.getNickName());
        if (UserConstants.SUPER_ADMIN_ID.equals(loginUser.getUserId())) {
            Set<String> menuPermission = new HashSet<>();
            menuPermission.add("*:*:*");
            sessionUser.setMenuPermission(menuPermission);
            Set<String> perms = new HashSet<>();
            perms.add(TenantConstants.SUPER_ADMIN_ROLE_KEY);
            sessionUser.setRolePermission(perms);
        } else {
            List<SysRole> list = sysRoleService.selectByUserId(loginUser.getUserId());
            JSONArray jsonArray = JSONUtil.parseArray(Optional.ofNullable(list).orElse(new ArrayList<>()));
            sessionUser.setRoles(JSONUtil.toList(jsonArray, RoleDTO.class));
            // 角色信息
            sessionUser.setRolePermission(Optional.ofNullable(list).orElse(new ArrayList<>()).stream().map(SysRole::getRoleKey).collect(Collectors.toSet()));
            // 角色权限信息
            if (CollUtil.isNotEmpty(list)) {
                List<SysMenu> menuList = sysMenuService.selectByRoleIds(list);
                sessionUser.setMenuPermission(Optional.ofNullable(menuList).orElse(new ArrayList<>()).stream().map(SysMenu::getPerms).collect(Collectors.toSet()));
            }
        }
        SaLoginModel model = new SaLoginModel();
        model.setDevice(saTokenModelConfig.getDevice());
        // 例如: 后台用户30分钟过期 app用户1天过期
        model.setTimeout(saTokenModelConfig.getTimeout());
        model.setActiveTimeout(saTokenModelConfig.getActiveTimeout());
        model.setExtra(LoginHelper.CLIENT_KEY, saTokenModelConfig.getClientId());
        LoginHelper.login(sessionUser, model);
        LoginViewBody loginView = new LoginViewBody();
        loginView.setAccessToken(StpUtil.getTokenValue());
        loginView.setExpireIn(StpUtil.getTokenTimeout());
        loginView.setClientId(saTokenModelConfig.getClientId());
        // 刷新登录时间
        threadPoolTaskExecutor.execute(() -> {
            Long userId = loginUser.getUserId();
            sysUserService.update(Wrappers.lambdaUpdate(SysUser.class).set(SysUser::getLoginDate, LocalDateTime.now()).eq(SysUser::getUserId, userId));
        });
        return loginView;
    }

    private void checkXssParams(LoginVo user) {
        // XSS攻击过滤
        String s = "﹤" + "script" + "﹥";
        s = Normalizer.normalize(s, Normalizer.Form.NFKC);
        // 使用正则表达式匹配inputStr是否存在<script>
        Pattern pattern = Pattern.compile(user.getLoginAccount());
        Matcher matcher = pattern.matcher(s);
        if (matcher.find()) {
            // Found black listed tag
            throw new BusinessSelfException("存在XSS攻击");
        }
        Pattern patternPwd = Pattern.compile(user.getPassword());
        Matcher matcherPwd = patternPwd.matcher(s);
        if (matcherPwd.find()) {
            // Found black listed tag
            throw new BusinessSelfException("存在XSS攻击");
        }
    }


    // 登出接口
    @SaCheckLogin
    @GetMapping("/user/logout")
    @Operation(summary = "登出接口")
    public Resp<LoginVo> logout(Model model, @RequestParam("id") Long userId) {
        Resp<LoginVo> resp = new Resp<>();
        StpUtil.logout(userId);
        resp.setCode(300);
        resp.setMsg("用户登出成功");
        return resp;
    }

    // 注销登录接口
    @SaCheckLogin
    @GetMapping("/user/offline")
    @Operation(summary = "注销登录--下线接口")
    public Resp<LoginVo> offline() {
        Resp<LoginVo> resp = new Resp<>();
        LoginUser loginUser = LoginHelper.getLoginUser();
        assert loginUser != null;
        StpUtil.logout(loginUser.getLoginId());
        resp.setCode(300);
        resp.setMsg("用户登出成功");
        return resp;
    }


    // 新增用户
    @SaIgnore
    @PostMapping("/user/saveUser")
    @Operation(summary = "新增用户")
    public Resp<Long> saveUser(@RequestBody SysUser sysUser) throws Exception {
        // RSA 私钥解密
        String aesBackPwd = RSAUtil.decryptByPrivateKey(RSAUtil.priKey, sysUser.getPassword());
        String password = SecureUtil.md5(aesBackPwd);
        sysUser.setPassword(password);
        // 校验是否用户已经存在
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_name", sysUser.getUserName());
        SysUser user = sysUserService.getOne(queryWrapper);
        if (user != null) {
            throw new BusinessSelfException("用户已经存在");
        }
        sysUserService.save(sysUser);
        return Resp.ok("Id->", sysUser.getUserId());
    }


    @PostMapping("/admin/check")
    @SaCheckRole("admin")
    @SaCheckLogin
    @Operation(summary = "测试一下自己的角色是否有效")
    public Resp<LoginUser> checkAdmin(Model model) {
        Resp<LoginUser> resp = new Resp<>();
        LoginUser loginUser = LoginHelper.getLoginUser();
        resp.setCode(100);
        resp.setMsg("管理员鉴权成功");
        resp.setData(loginUser);
        return resp;
    }

    /**
     * ShearCaptcha
     * 图片格式
     * session存储
     * 接口需添加白名单放行
     *
     * @param request HttpServletRequest
     */
    @GetMapping("/verify")
    @Operation(summary = "生成验证码")
    public void verify(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("image/jpeg");
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
        //定义图形验证码的长、宽、验证码字符数、干扰线宽度
        ShearCaptcha shearCaptcha = CaptchaUtil.createShearCaptcha(150, 40, 5, 4);
        //图形验证码写出，可以写出到文件，也可以写出到流
        shearCaptcha.write(response.getOutputStream());
        // 开启验证码
        if (enableVerifyCode) {
            String remoteAddr = getIpAddress(request);
            //获取验证码中的文字内容
            redisTemplate.opsForValue().set(remoteAddr + "_verifyCode", shearCaptcha.getCode(), 3, TimeUnit.MINUTES);
        }
    }

    private static final String UNKNOWN = "unknown";
    private static final String LOCALHOST = "127.0.0.1";
    private static final String SEPARATOR = ",";

    public String getIpAddress(HttpServletRequest request) {
        String ipAddress = extractHeader(request, "x-forwarded-for");
        if (isUnknown(ipAddress)) {
            ipAddress = extractHeader(request, "Proxy-Client-IP");
        }
        if (isUnknown(ipAddress)) {
            ipAddress = extractHeader(request, "WL-Proxy-Client-IP");
        }
        if (isUnknown(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            if (LOCALHOST.equals(ipAddress)) {
                try {
                    ipAddress = InetAddress.getLocalHost().getHostAddress();
                } catch (UnknownHostException e) {
                    log.error("Unknown Host Exception", e);
                }
            }
        }
        return parseIp(ipAddress);
    }

    private String extractHeader(HttpServletRequest request, String headerName) {
        return Optional.ofNullable(request.getHeader(headerName)).orElse(UNKNOWN);
    }

    private boolean isUnknown(String ipAddress) {
        return StringUtils.isBlank(ipAddress) || UNKNOWN.equalsIgnoreCase(ipAddress);
    }

    private String parseIp(String ipAddress) {
        if (ipAddress != null && ipAddress.length() > 15 && ipAddress.contains(SEPARATOR)) {
            return ipAddress.substring(0, ipAddress.indexOf(","));
        }
        return ipAddress;
    }


    public String getOs(HttpServletRequest request) {
        String osType = request.getHeader("User-Agent");
        if (osType.contains("Windows")) {
            return "Windows PC";
        } else if (osType.contains("Mac")) {
            return "Mac PC";
        } else {
            return "Mobile Device";
        }
    }
}